/*
 * To veranderOps this template, choose Tools | Templates
 * and open the template in the editor.
 */
package be.hogent.iii.project.server.netwerk;

import be.hogent.iii.project.common.bericht.SpelenLijstBericht;
import be.hogent.iii.project.common.bericht.SpelersLijstBericht;
import be.hogent.iii.project.common.bericht.WeggevallenClientBericht;
import be.hogent.iii.project.common.gameroom.objecten.GameRoomData;
import com.thoughtworks.xstream.XStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
import java.util.Map.Entry;

/**
 *
 * @author Dieter Van der Stock
 */
public class Server implements Runnable {

    private InetAddress hostAdres;
    private int poort;
    private ServerSocketChannel serverKanaal; //hiermee aanvaarden we connecties
    private Selector selector; //de 'core' van alles, houdt alle connecties bij
    private ByteBuffer leesBuffer = ByteBuffer.allocate(16 * 1024 * 1024); //de buffer waar we gaan inlezen, 8MB
    private VerwerkXMLWorker werker; //de klasse die gaat bepalen wat er moet geantwoord worden ed
    private List updateLijst = new LinkedList(); //gelinkte lijst met veranderingen die moeten gedaan worden
    private Map dataMap;//gaat bijhouden welke data (in een queue) hoort bij welk afzenderSocket om te verzenden
    private Map poolMap;//linkt de namen van pools met het poolobject
    private Map alleClients; // mooi voor overzicht en handig voor naar 1 specifieke nickname te sturen
    private static Server server;

    /**
     * Gaat een serverobject aanmaken op een bepaalde poort, en linkt een werkerthread aan de server
     * @param hostAdres
     * @param poort
     * @param werker
     * @throws java.io.IOException
     */
    private Server(InetAddress hostAdres, int poort, VerwerkXMLWorker werker) throws IOException {
        this.hostAdres = hostAdres;
        this.poort = poort;
        this.selector = this.initSelector();
        this.werker = werker;
        this.alleClients = new HashMap();
        this.poolMap = new HashMap();
        this.dataMap = new HashMap();
    }

    /**
     * Implementatie van het singleton pattern 
     * @return Het server object
     */
    public static synchronized Server getServer() {
        if (server == null) {
            try {
                VerwerkXMLWorker werker = new VerwerkXMLWorker();
                new Thread(werker).start();
                server = new Server(null, 9090, werker);

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return server;
    }

    //nodig voor het singletonpattern te handhaven
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /**
     * Gaat data klaarzetten om te versturen naar een bepaalde socket, en de selector wakker maken om dit uit te voeren.
     * @param socketKanaal
     * @param data
     */
    public void stuurBericht(SocketChannel socketKanaal, byte[] data) {

        System.out.println("##################");
        System.out.println("Data die verzonden wordt naar " + getNicknameVanSocket(socketKanaal));
        System.out.println(new String(data));
        System.out.println("################");

        //we willen een bericht naar een bepaalde afzenderSocket versturen. Hiervoor veranderen we eerst de OPS van die afzenderSocket in WRITE 
        synchronized (this.updateLijst) {
            //dit doen we door toe te voegen aan een veranderingenlijst
            this.updateLijst.add(new updateOpsAanvraag(socketKanaal, updateOpsAanvraag.CHANGEOPS, SelectionKey.OP_WRITE));

            // Daarna koppelen we de data die we willen schrijven met het afzenderSocket waarnaar we willen schrijven in de dataMap
            synchronized (this.dataMap) {
                List queue = (List) this.dataMap.get(socketKanaal);
                if (queue == null) { //als er nog geen queue van data voor dat afzenderSocket bestaat maken we er een
                    queue = new ArrayList();
                    this.dataMap.put(socketKanaal, queue);
                }
                queue.add(ByteBuffer.wrap(data)); //we steken de data die intussen naar die client moet worden gestuurd in de queue, die ook in de datamap staat
            }
        }

        // We maken de selector wakker zodat de updates kunnen doorgevoerd worden
        // (dan gaat de run zien dat er write-keys zijn, daaropvolgend de verzendData() oproepen en de dataMap van hierboven afhandelen
        this.selector.wakeup();
    }

    /**
     * Het hart van heel het serverproces. Zal blijven lopen en voortdurend kijken naar nieuwe uit te voeren acties.
     */
    public void run() {
        System.out.println("Server succesvol opgestart, we luisteren op poort " + poort + " voor binnenkomende connecties en data.");
        while (true) {
            try {
                // nieuwe veranderingen doorvoeren
                synchronized (this.updateLijst) {
                    Iterator updates = this.updateLijst.iterator();
                    while (updates.hasNext()) {
                        //elke verandering steken we in een updateOpsAanvraag
                        updateOpsAanvraag veranderOps = (updateOpsAanvraag) updates.next();
                        switch (veranderOps.type) {
                            case updateOpsAanvraag.CHANGEOPS:
                                SelectionKey key = veranderOps.socketKanaal.keyFor(this.selector);
                                if (key != null) { //een bug met een disconnectende client, is hiermee opgelost.
                                    key.interestOps(veranderOps.ops);
                                }
                        }
                    }
                    this.updateLijst.clear();
                }

                // We wachten tot er iets gebeurt (select() gaat blocken tot het zover is)
                this.selector.select();

                // We vragen een set van alle events (keys) op en behandelen ze één voor één
                Iterator selectedKeys = this.selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    SelectionKey key = (SelectionKey) selectedKeys.next();
                    selectedKeys.remove();

                    //als de key om één of andere reden niet deugd, skippen we deze iteratie
                    if (!key.isValid()) {
                        continue;
                    }

                    // Zien welk soort event we te pakken hebben in onze key en de juiste procedure oproepen
                    if (key.isAcceptable()) {
                        this.aanvaardConnectie(key); //nieuwe connectieaanvraag
                    } else if (key.isReadable()) {
                        this.aanvaardData(key); //we krijgen data binnen
                    } else if (key.isWritable()) {
                        this.verzendData(key); //we kunnen data wegschrijven naar een kanaal (er is data beschikbaar, zie 'stuurbericht()'
                    }
                }
            } catch (Exception e) {
                System.out.println("Er is een fout opgetreden in run(): " + e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * Gaat een aanvraag tot connectie afhandelen.
     * 
     * @param key De key van de selector waarin de aanvraag zit.
     * @throws java.io.IOException
     */
    private void aanvaardConnectie(SelectionKey key) throws IOException {
        // Als er een verbinding staat te wachten, betekent dat dat het kanaal ons serverkanaal moet zijn
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

        // Aanvaard de verbinding en maak het non-blocking
        SocketChannel socketKanaal = serverSocketChannel.accept();
        socketKanaal.configureBlocking(false);

        //deze socket zoiezo toevoegen aan onze collectie verbonden clients
        //die 'naam', zijnde hier het adres van de client, wordt in later stadium vervangen door de nickname
        alleClients.put(socketKanaal.toString(), socketKanaal);

        // We registreren onze nieuwe connectie en laten blijken dat we geïntresseerd zijn in leesbare data
        socketKanaal.register(this.selector, SelectionKey.OP_READ);

        System.out.println("We hebben een nieuwe client op" + socketKanaal.socket().getRemoteSocketAddress());
    }

    /**
     * Gaat data aanvaarden wanneer de selector meldt dat er iets te lezen is.
     * @param key De key van de selector waar we data kunnen afhalen.
     * @throws java.io.IOException
     */
    private void aanvaardData(SelectionKey key) throws IOException {
        SocketChannel afzenderSocket = (SocketChannel) key.channel();

        // Voor de zekerheid onze leesbuffer leegmaken voor we er iets nieuw insteken
        this.leesBuffer.clear();

        // Proberen data te lezen van de afzenderSocket
        int aantalBytesGelezen;
        try {
            aantalBytesGelezen = afzenderSocket.read(this.leesBuffer);
        } catch (IOException e) {
            //als er iets misloopt, verwijderen we de aanvraag en de client van de server (ook in alle lijsten, pools, etc)
            String nickname = getNicknameVanSocket(afzenderSocket);
            System.out.println("Bij een client liep er plots iets mis. We cancellen de aanvraag en sluiten het kanaal naar " + nickname);
            key.cancel();
            behandelWeggevallenClient(nickname);
            afzenderSocket.close();
            return;
        }

        if (aantalBytesGelezen == -1) {
            //als er iets misloopt, verwijderen we de aanvraag en de client van de server (ook in alle lijsten, pools, etc)
            String nickname = getNicknameVanSocket(afzenderSocket);
            System.out.println("Een client heeft zijn verbinding afgesloten. We cancellen de aanvraag en sluiten het kanaal naar " + nickname);
            key.cancel();
            behandelWeggevallenClient(nickname);
            afzenderSocket.close();
            return;
        }

        //iedereen wil wel wat bevestiging in het leven..
        String msg = new String(leesBuffer.array(), 0, aantalBytesGelezen);
        System.out.println("We kregen een bericht van " + getNicknameVanSocket(afzenderSocket) + "-> " + msg);

        //het binnengekregen bericht naar de werker sturen, die zal het verder behandelen
        this.werker.verwerkData(this, getNicknameVanSocket(afzenderSocket), this.leesBuffer.array(), aantalBytesGelezen);

    }

    

    /**
     * Gaat de data daadwerkelijk verzenden wanneer het kanaal klaar is volgens de selector.
     * @param key De key van de selector waar de info in staat waar we naar kunnen sturen.
     * @throws java.io.IOException
     */
    private void verzendData(SelectionKey key) throws IOException {
        SocketChannel socketKanaal = (SocketChannel) key.channel();

        //we handelen de datamap af die in 'stuurbericht' wordt toegevoegd
        synchronized (this.dataMap) {
            List queue = (List) this.dataMap.get(socketKanaal);

            // blijven schrijven tot heel de queue voor dat afzenderSocket leeg is
            while (!queue.isEmpty()) {
                ByteBuffer buf = (ByteBuffer) queue.get(0);
                System.out.println("We sturen " + "een bericht" + " naar " + socketKanaal.socket().getRemoteSocketAddress());
                socketKanaal.write(buf);
                if (buf.remaining() > 0) {
                    //of tot dat de socketbuffer van het OS vol zit
                    break;
                }
                queue.remove(0);

            }

            //alles is geschreven, we zetten het terug naar read (hier niet met updates ofzo, want het gebeurt in dezelfde thread
            if (queue.isEmpty()) {
                key.interestOps(SelectionKey.OP_READ);
            }
        }
    }

    /**
     * Zal de selector, het hart van ons netwerk, opstarten en teruggeven.
     * @return De gemaakte selector
     * @throws java.io.IOException
     */
    private Selector initSelector() throws IOException {
        // We maken een selector aan 
        Selector socketSelector = Selector.open();

        // We maken het serverkanaal aan en configureren het als Non-Blocking
        this.serverKanaal = ServerSocketChannel.open();
        serverKanaal.configureBlocking(false);

        // We binden het kanaal tot het juiste adres en poort
        serverKanaal.socket().bind(new InetSocketAddress(this.hostAdres, this.poort));

        // We registreren dit serverkanaal bij de selector waarbij we laten blijken dat we
        // geïntresseerd zijn in binnenkomende connecties
        serverKanaal.register(socketSelector, SelectionKey.OP_ACCEPT);

        return socketSelector;
    }

    /**
     * Zal een client kunnen toevoegen aan een pool (en de pool creeëren als die nog niet bestaat)
     * @param poolNaam De naam van de pool waar we deze persoon willen toevoegen
     * @param nickname De nickname van de persoon die we willen toevoegen
     * @param socketKanaal De socket van de client
     */
    public void voegClientToeAanPool(String poolNaam, String nickname, SocketChannel socketKanaal) {
        //pool die is opgegeven bestaat al: we voegen de client toe
        if (poolMap.containsKey(poolNaam)) {
            //we nemen de desbetreffende pool eruit, voegen de client toe en steken de pool terug
            ((Pool) poolMap.get(poolNaam)).voegClientToe(new Client(nickname, socketKanaal));
        } //opgegeven pool bestaat nog niet: we maken hem aan, voegen die client toe, en steken hem in de map
        else {
            System.out.println("We maken een nieuwe pool: " + poolNaam);
            Pool nieuwePool = new Pool(poolNaam);
            Client add = new Client(nickname, socketKanaal);
            nieuwePool.voegClientToe(add);
            poolMap.put(poolNaam, nieuwePool);
        }
        System.out.println(nickname + " werd toegevoegd aan de pool " + poolNaam);
    }

    /**
     * Zal een client aan een pool toevoegen (zonder dat de socket moet opgegeven worden, die wordt opgezocht)
     * @param poolNaam De naam van de pool waar we toevoegen
     * @param nickname De nickname van de persoon die we willen toevoegen
     */
    public void voegClientToeAanPool(String poolNaam, String nickname) {
        SocketChannel socket = getSocketVanClient(nickname);
        if (socket != null) { //we hate nullpointerexceptions!
            voegClientToeAanPool(poolNaam, nickname, socket);
        }
    }

    /**
     * Maakt een pool aan met de opgegeven naam
     * @param poolNaam De naam van de nieuwe pool
     */
    public void maakPool(String poolNaam) {
        //bestaat de pool al?
        if (!poolMap.containsKey(poolNaam)) {
            poolMap.put(poolNaam, new Pool(poolNaam));
        }
    }

    /**
     * Zal een client met een opgegeven nickname verwijderen uit een opgegeven pool.
     * @param poolNaam
     * @param nickname
     */
    public void verwijderClientUitPool(String poolNaam, String nickname) {
        //pool die is opgegeven bestaat: we verwijderen de client. Zoniet, no harm done
        if (poolMap.containsKey(poolNaam)) {
            //we nemen de desbetreffende pool eruit, verwijderen de client en steken de pool terug
            ((Pool) poolMap.get(poolNaam)).verwijderClient(nickname);
        }
        System.out.println(nickname + " werd verwijderd uit pool " + poolNaam);
    }

    /**
     * Gaat een client met een opgegeven nickname compleet verwijderen van de server.
     * @param nickname
     */
    public void verwijderClientVanServer(String nickname) {
        System.out.println("We verwijderen " + nickname + " van de server.");
        //zoiezo al uit 'alleclients' verwijderen
        alleClients.remove(nickname);

        //nu ook nog verwijderen in elke pool 
        Iterator it = poolMap.keySet().iterator();
        while (it.hasNext()) {
            //bij elke pool proberen we te verwijderen. Zit em er niet in, no harm done
            String naamPool = (String) it.next();
            verwijderClientUitPool(naamPool, nickname);
        }
    }
    
    /**
     * Doet wat er moet gebeuren als er een client's netwerkverbinding wegvalt 
     * of de client afgesloten wordt.
     * @param nickname De persoon die is weggevallen
     */
    private void behandelWeggevallenClient(String nickname) {

        //we maken een bericht waarin staat wie is weggevallen
        WeggevallenClientBericht bericht = new WeggevallenClientBericht(nickname);
        XStream xStream = new XStream();
        String msg = xStream.toXML(bericht);

        //zoiezo al uit 'alleclients' verwijderen
        alleClients.remove(nickname);

        //elke pool overlopen, hem eruit verwijderen en naar de overige leden sturen dat hij weg is
        Iterator it = poolMap.values().iterator();
        while (it.hasNext()) {
            Pool pool = (Pool) it.next();
            //zit hij in deze pool?
            if (pool.bevatClient(nickname)) {
                System.out.println("We laten de mensen in pool " + pool.getNaam() + " weten dat " + nickname + " is weggevallen");
                verwijderClientUitPool(pool.getNaam(), nickname);

                if(GameRoomData.getGameRoomData().getSpelerslijst().getSpelerslijst() != null){
                for (int i = 0; i < GameRoomData.getGameRoomData().getSpelerslijst().getSpelerslijst().size(); i++) {
                    if (GameRoomData.getGameRoomData().getSpelerslijst().getSpelerslijst().get(i).getNick().equals(nickname)) {
                        GameRoomData.getGameRoomData().getSpelerslijst().getSpelerslijst().remove(GameRoomData.getGameRoomData().getSpelerslijst().getSpelerslijst().get(i));
                    }
                }
                }
                if(GameRoomData.getGameRoomData().getSpelerslijst().getSpelerslijst() != null && GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst() != null){
                for (int i = 0; i < GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().size(); i++) {
                    for (int j = 0; j < GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().get(i).getDeelnemers().size(); j++) {
                        if (GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().get(i).getDeelnemers().get(j).getNick().equals(nickname)) {
                            GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().get(i).getDeelnemers().remove(GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().get(i).getDeelnemers().get(j));
                        }
                        
                    }
                    if (GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().get(i).getEigenaar().equals(nickname)) {
                            GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().remove(GameRoomData.getGameRoomData().getSpelenlijst().getSpelenlijst().get(i));
                        }
                }}

                SpelenLijstBericht spelen = new SpelenLijstBericht(GameRoomData.getGameRoomData().getSpelenlijst());
                SpelersLijstBericht spelers = new SpelersLijstBericht(GameRoomData.getGameRoomData().getSpelerslijst());
                stuurBerichtNaarPool(pool.getNaam(), xStream.toXML(spelen));
                stuurBerichtNaarPool(pool.getNaam(), xStream.toXML(spelers));
                stuurBerichtNaarPool(pool.getNaam(), msg);
                verwijderPool(pool.getNaam());
            }
        }


    }

    /**
     * Verandert een nickname, overal waar hij voorkomt op onze server
     * @param vorige
     * @param nieuwe
     */
    public void veranderNickname(String vorige, String nieuwe) {
        //uit alleclients de nieuwe toevoegen en de vorige verwijderen
        SocketChannel kanaal = (SocketChannel) alleClients.get(vorige);
        alleClients.put(nieuwe, kanaal);
        alleClients.remove(vorige);

        /*//alle pools aflopen en daar de nickname veranderen 
        Iterator it = poolMap.keySet().iterator();
        while(it.hasNext()) {
        //bij elke pool proberen we de nickname te veranderen. Zit em er niet in, no harm done
        //((Pool)it.next()).veranderNickname(vorige, nieuwe);
        }*/

        //eigenlijk enkel in mainChat dat dat nickname veranderen wordt gebruikt. De code
        // bestaat nu, maar zolang we niet alle pools moeten itereren, doen we dat best ook niet
        ((Pool) poolMap.get("mainChat")).veranderNickname(vorige, nieuwe);
    }

    /**
     * Zal een bericht naar een specifieke client sturen met de opgegeven nickname
     * @param nickname
     * @param bericht
     */
    public void stuurBerichtNaarClient(String nickname, String bericht) {
        //als we zo'n client hebben, sturen we ernaar. Zoniet zal die wrs niet meer verbonden zijn
        //en negeren we het bericht
        if (alleClients.containsKey(nickname)) {
            stuurBericht((SocketChannel) alleClients.get(nickname), bericht.getBytes());
        }
    }

    /**
     * Zal een bericht naar alle leden van een bepaalde pool sturen
     * @param poolNaam
     * @param bericht
     */
    public void stuurBerichtNaarPool(String poolNaam, String bericht) {
        System.out.println("We sturen een bericht naar " + poolNaam);
        //alle sockets uit die pool halen
        Pool pool = (Pool) poolMap.get(poolNaam);
        if (pool != null) {
            ArrayList sockets = pool.getSockets();

            //voor elk van die sockets het stuurBericht oproepen
            Iterator it = sockets.iterator();
            while (it.hasNext()) {
                stuurBericht((SocketChannel) it.next(), bericht.getBytes());
            }
        }
    }
    
        /**
     * We geven een socket op, en krijgen de bijhorende nickname terug
     * @param socket
     * @return De nickname van de persoon, horende bij die socket
     */
    public String getNicknameVanSocket(SocketChannel socket) {
        Set paren = alleClients.entrySet();
        Iterator it = paren.iterator();
        Entry ent = null;
        Boolean gevonden = false;
        String naam = "";
        while (it.hasNext() && !gevonden) {
            ent = (Entry) it.next();
            if ((SocketChannel) ent.getValue() == socket) {
                gevonden = true;
                naam = (String) ent.getKey();
            }
        }
        return naam;
    }

    /**
     * Gaat de socket teruggeven die bij de gegeven nickname hoort. null als die niet bestaat.
     * @param nickname De persoon van wie we de socket willen
     * @return Het socketkanaal dat we terugsturen
     */
    public SocketChannel getSocketVanClient(String nickname) {
        return (SocketChannel) alleClients.get(nickname);
    }

    /**
     * We verwijderen een pool van de server
     * @param poolNaam De pool die we willen verwijderen
     */
    public void verwijderPool(String poolNaam) {
        poolMap.remove(poolNaam);
    }

    /**
     * Geeft de pool terug met de opgegeven naam
     * @param poolNaam
     * @return De gevraagde pool
     */
    public Pool getPool(String poolNaam) {
        return (Pool) poolMap.get(poolNaam);
    }

    /**
     * Geeft een lijst van nicknames terug die in de opgegeven pool zitten
     * @param poolNaam
     * @return De lijst met nicknames
     */
    public List getNicknamesInPool(String poolNaam) {
        return ((Pool) poolMap.get(poolNaam)).getNicknames();
    }

    /**
     * Geeft alle sockets terug van alle mensen die in de opgegeven pool zitten
     * @param poolNaam
     * @return De lijst met alle sockets
     */
    public List getSocketsInPool(String poolNaam) {
        return ((Pool) poolMap.get(poolNaam)).getSockets();
    }

    /**
     * Laat onze server runnen.
     * @param args
     */
    public static void main(String[] args) {

        System.out.println("Welkom op onze server.");

        new Thread(Server.getServer()).start();

    }
}
